using System;
using System.Collections;
using System.Collections.Specialized;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Reflection;
using System.Windows.Threading;
using System.Xml.Serialization;
using PDO.DataPersistence.ServiceProvider;
using PDO.Utilities;
using System.Linq.Dynamic;




namespace PDO.DataPersistence.Core
{

    public class InvokeOC<T> : ObservableCollection<T>
    {
        private Dispatcher dispatcherUIThread;

        private delegate void SetItemCallback(int index, T item);
        private delegate void RemoveItemCallback(int index);
        private delegate void ClearItemsCallback();
        private delegate void InsertItemCallback(int index, T item);
        private delegate void MoveItemCallback(int oldIndex, int newIndex);

        public InvokeOC(Dispatcher dispatcher)
        {
            this.dispatcherUIThread = dispatcher;
        }

        protected override void SetItem(int index, T item)
        {
            if (dispatcherUIThread.CheckAccess())
            {
                base.SetItem(index, item);
            }
            else
            {
                dispatcherUIThread.Invoke(DispatcherPriority.Send,
                    new SetItemCallback(SetItem), index, new object[] { item });
            }
        }

        protected override void RemoveItem(int index)
        {
            if (dispatcherUIThread.CheckAccess())
            {
                base.RemoveItem(index);
            }
            else
            {
                dispatcherUIThread.Invoke(DispatcherPriority.Send,
                    new RemoveItemCallback(RemoveItem), index);
            }
        }

        protected override void ClearItems()
        {
            if (dispatcherUIThread.CheckAccess())
            {
                base.ClearItems();
            }
            else
            {
                dispatcherUIThread.Invoke(DispatcherPriority.Send,
                    new ClearItemsCallback(ClearItems));
            }
        }

        protected override void InsertItem(int index, T item)
        {
            if (dispatcherUIThread.CheckAccess())
            {
                base.InsertItem(index, item);
            }
            else
            {
                dispatcherUIThread.Invoke(DispatcherPriority.Send,
                    new InsertItemCallback(InsertItem), index, new object[] { item });
            }
        }

        protected override void MoveItem(int oldIndex, int newIndex)
        {
            if (dispatcherUIThread.CheckAccess())
            {
                base.MoveItem(oldIndex, newIndex);
            }
            else
            {
                dispatcherUIThread.Invoke(DispatcherPriority.Send,
                    new MoveItemCallback(MoveItem), oldIndex, new object[] { newIndex });
            }
        }
    }


    public class PersistableDataObjectList<T> : ObservableCollection<T>, IDisposable, IPersistableDataObjectList
        where T : PersistableDataObject, new()
    {

        private bool _loadingFromDataSource; // set true to disable read-only checking when loading from database
        private bool _changeTrackingEnabled;

        public bool LoadingFromDataSource
        {
            get { return _loadingFromDataSource; }
            set { _loadingFromDataSource = value; }
        }


        /// <summary>
        /// Is change tracking enabled for this object.  True by default.
        /// </summary>
        [XmlIgnore]
        public bool ChangeTrackingEnabled
        {
            get { return _changeTrackingEnabled; }
            set { _changeTrackingEnabled = value; }
        }



        #region Private Members

        private bool _isNew;
        private bool _isDirty;
        private bool _updateAvailable;

        private PersistableDataObject _parent;

        private string _parentForeignKeyName;
        private string _sourcePrimaryKeyName;
        private bool _isImmutable;
        private DataSession _session;
        private static bool _busy;
        private bool _markedForDeletion;
        private string _dbKey;
        private Dispatcher _dispatcher;


        // Information needed to load or re-load list
        private CommandType _loadCommand;
        private string _loadSql;
        private object[] _loadParameterValues;

        // set true to indicate a query returned more than PageSize records.
        private bool _isOverflow;
        private int _totalItemCount;

        private bool _isIQueryable;
        private IQueryable _query;

        private bool _sendNotificationsWhileLoading;

        // thread sync
        [XmlIgnore]
        public Object SyncRoot { get; set; }

        // List of items deleted from list main list
        private PersistableDataObjectList<T> _deletedItems;

        // property change notification target
        private object _notificationObject;

        private PersistableDataObject _rootParent;
        private int _nestingLevel = -1;
        private bool _isSelfReference;

        // Paged loading support
        private int _pageSize;

        private bool _isMultiPaged;
        private bool _isLastPage;
        private int _currentPage; // page numbering starts at 1
        private int _pageCount;



        //Sorting
        private IComparer _defaultComparer;

        #endregion

        #region Constructors

        public PersistableDataObjectList() // was InvokeOC base : base(Dispatcher.CurrentDispatcher) -- but not neccessary currently
        {
            _isNew = true;

            _pageCount = 1;
            _currentPage = 1;

            _dispatcher = Dispatcher.CurrentDispatcher;
            SyncRoot = new Object();
        }

        public PersistableDataObjectList(PersistableDataObjectList<T> list)
            : this()
        {
            _parentForeignKeyName = list._parentForeignKeyName;
            _sourcePrimaryKeyName = list._sourcePrimaryKeyName;

            _loadCommand = list._loadCommand;
            _loadParameterValues = list._loadParameterValues.ToArray<object>();
            _loadSql = list._loadSql;
            _isIQueryable = list._isIQueryable;
            _query = list._query;
        }


        public PersistableDataObjectList(DataSession session, PersistableDataObject parent,
            string parentForeignKeyName, string sourcePrimaryKeyName,
             CommandType command, string sql, params object[] parameterValues)
            : this()
        {
            _session = session;

            _parent = parent;

            _parentForeignKeyName = parentForeignKeyName;
            _sourcePrimaryKeyName = sourcePrimaryKeyName;
            _loadCommand = command;
            _loadSql = sql;
            _loadParameterValues = parameterValues;

            if (null != parent)
            {
                IsImmutable = parent.IsImmutable;
            }

        }

        public PersistableDataObjectList(IQueryable query)
            : this(DataSession.DefaultSession, null, null, null, query)
        {
        }


        public PersistableDataObjectList(DataSession session, PersistableDataObject parent,
            string parentForeignKeyName, string sourcePrimaryKeyName,
             IQueryable query)
            : this()
        {
            _isIQueryable = true;
            _query = query;

            _session = session;
            _parent = parent;

            _parentForeignKeyName = parentForeignKeyName;
            _sourcePrimaryKeyName = sourcePrimaryKeyName;
            _loadCommand = CommandType.Text;

            System.Data.Common.DbCommand cmd = DataSession.LINQDataContext.GetCommand(query);
            if (cmd.Parameters.Count > 0)
            {
                List<object> parameters = new List<object>();
                foreach (System.Data.Common.DbParameter param in cmd.Parameters)
                {
                    parameters.Add(new SqlParameter(param.ParameterName, param.Value));
                }

                _loadParameterValues = parameters.ToArray();
            }

            _loadSql = query.ToString();

            if (null != parent)
            {
                IsImmutable = parent.IsImmutable;
            }

        }


        #endregion

        #region Public Properties and Methods

        [XmlIgnore]
        public bool IsDirty
        {
            get { return _isDirty; }
            set { _isDirty = value; }
        }

        [XmlIgnore]
        public bool IsNew
        {
            get { return _isNew; }
            set { _isNew = value; }
        }

        [XmlIgnore]
        public string SourceDatabaseTableName
        {
            get
            {
                return PDOMapper.GetSourceDatabaseTableName(typeof(T));
            }
        }

        [XmlIgnore]
        public DataSession Session
        {
            get { return _session; }
            set { _session = value; }
        }


        [XmlIgnore]
        public bool SendNotificationsWhileLoading
        {
            get { return _sendNotificationsWhileLoading; }
            set { _sendNotificationsWhileLoading = value; }
        }


        [XmlIgnore]
        public bool IsImmutable
        {
            get { return _isImmutable; }
            set { _isImmutable = value; }
        }

        /// <summary>
        /// Returns unique identifiying key.
        /// </summary>
        [XmlIgnore]
        public string DBKey
        {
            get
            {
                if (null == _dbKey)
                {
                    _dbKey = _loadSql + ":" + ((null != _loadParameterValues) ? _loadParameterValues.ToString() :
                        Guid.Empty.ToString());
                }
                return _dbKey;
            }
            private set { _dbKey = value; }
        }

        /// <summary>
        /// Refresh list contents by reloading from database, issuing change notifications as appropriate.
        /// </summary>
        public void Refresh()
        {
            Load();
        }

        #region Paging Support

        /// <summary>
        /// Number of items loaded per database access.  Defaults to Session.DefaultPageSize.
        /// </summary>
        public int PageSize
        {
            get
            {
                if (0 == _pageSize)
                {
                    _pageSize = Session.DefaultPageSize;
                }
                return _pageSize;
            }
            set
            {
                _pageSize = value;
                OnPropertyChanged(new PropertyChangedEventArgs("PageSize"));
            }
        }

        /// <summary>
        /// Current page in paged-load (starts at 1).
        /// </summary>
        public int CurrentPage
        {
            set
            {
                _currentPage = value;
                OnPropertyChanged(new PropertyChangedEventArgs("CurrentPage"));
                OnPropertyChanged(new PropertyChangedEventArgs("IsFirstPage"));
            }
            get
            {
                return _currentPage;
            }
        }

        /// <summary>
        /// Is this the first page?
        /// </summary>
        public bool IsFirstPage
        {
            get { return _currentPage == 1; }
        }

        /// <summary>
        /// Is this the last page?
        /// </summary>
        public bool IsLastPage
        {
            set
            {
                _isLastPage = value;
                OnPropertyChanged(new PropertyChangedEventArgs("IsFirstPage"));
            }
            get { return _isLastPage; }
        }

        /// <summary>
        /// Is list multi-paged?  I.e. is total length of list greater than page size.
        /// </summary>
        public bool IsMultiPaged
        {
            set
            {
                _isMultiPaged = value;
                OnPropertyChanged(new PropertyChangedEventArgs("IsMultiPaged"));
            }
            get { return _isMultiPaged; }
        }

        /// <summary>
        /// Total count of pages in list. 
        /// </summary>
        public int PageCount
        {
            set
            {
                _pageCount = value;
                OnPropertyChanged(new PropertyChangedEventArgs("PageCount"));
            }
            get { return _pageCount; }
        }

        /// <summary>
        /// Confirm that the list was created with paging support; i.e. that it is built on an IQueryable.
        ///
        /// Exceptions:
        ///   System.InvalidOperationException:
        ///     list not based on IQueryable.
        /// </summary>
        private void ConfirmPagingSupported()
        {
            if (!_isIQueryable)
            {
                throw new InvalidOperationException("Paging is only supported on IQueryable based lists.");
            }
        }


        private void UpdatePagedList()
        {
            _busy = true;

            Clear();
            Load();

            // Need to replace this with something to get total row count when we first 
            // determine that this listed is multi-paged.  
            // Issue a separate database query with a modified IQueryable...
            if (CurrentPage >= PageCount)
            {
                PageCount = CurrentPage;
            }
        }


        /// <summary>
        /// Jump to first list page. 
        /// </summary>
        /// <returns>true if more pages remain; false if on last page</returns>
        public void MoveFirstPage()
        {
            ConfirmPagingSupported();

            if (!IsMultiPaged)
            {
                return;
            }

            CurrentPage = 1;

            UpdatePagedList();
        }

        /// <summary>
        /// Jump to last page. 
        /// </summary>
        /// <returns>true if more pages remain; false if on last page</returns>
        public void MoveLastPage()
        {
            ConfirmPagingSupported();

            if (!IsMultiPaged)
            {
                return;
            }

            CurrentPage = PageCount;

            UpdatePagedList();
        }


        /// <summary>
        /// Advance to next list page. 
        /// </summary>
        /// <returns>true if more pages remain; false if on last page</returns>
        public void MoveNextPage()
        {
            ConfirmPagingSupported();

            if (!IsMultiPaged)
            {
                return;
            }
            CurrentPage++;

            UpdatePagedList();
        }


        /// <summary>
        /// Move to previous list page. 
        /// </summary>
        /// <returns>true if more pages remain; false if on last page</returns>
        public void MovePreviousPage()
        {
            ConfirmPagingSupported();

            if (!IsMultiPaged || CurrentPage <= 1)
            {
                return;
            }

            CurrentPage--;

            UpdatePagedList();
        }

        #endregion

        /// <summary>
        /// If true, indicates collection is busy, for instance in the process of loading.
        /// </summary>
        [XmlIgnore]
        public bool Busy
        {
            get { return _busy; }
        }

        [XmlIgnore]
        public bool MarkedForDeletion
        {
            get { return _markedForDeletion; }
            set { _markedForDeletion = value; ; }
        }


        /// <summary>
        /// Mark all items in list to be deleted at next save.
        /// </summary>
        public void MarkAllForDeletion()
        {
            using (TimedLock.Lock(SyncRoot))
            {
                foreach (PersistableDataObject pdo in Items)
                {
                    pdo.MarkedForDeletion = true;
                }
                this.MarkedForDeletion = true;
            }
        }

        /// <summary>
        /// Returns the item from list with a matching primary key value.
        /// </summary>
        /// <param name="pkeyID">Primary key value</param>
        /// <returns>PersistableDataObject or null if not found</returns>
        public PersistableDataObject GetItemByPKeyID(object pkeyID)
        {

            // Make sure we're not dealing with a new, uninitialized Guid primary key.
            // New Guid's are likely to cause problems by having multiple matches, which 
            // under normal operation we shouldn't have to worry about.
            Assert.IsFalse(pkeyID is Guid && pkeyID.ToString() == Guid.Empty.ToString());


            foreach (PersistableDataObject pdo in Items)
            {
                if (pdo.PKeyID.ToString().Equals(pkeyID.ToString()))
                {
                    return pdo;
                }
            }
            return null;
        }


        public PersistableDataObject GetItemByHashCode(object pdoTarget)
        {
            foreach (PersistableDataObject pdo in Items)
            {
                if (pdo.GetHashCode().Equals(pdoTarget.GetHashCode()))
                {
                    return pdo;
                }
            }
            return null;
        }


        /// <summary>
        /// Find nesting level and top-level (root) parent.
        /// </summary>
        private void FindHierarchyPosition()
        {
            // This implementation differs from that in PersistableDataObject in that it
            // starts recursing up the heirarchy from the *parent* of this list instead of 
            // directly from the object itself.

            if (_nestingLevel == -1)
            {
                _nestingLevel = 0;
                _rootParent = this.Parent; // start with parent of this list
                while (null != _rootParent && null != _rootParent.Parent)
                {
                    _nestingLevel++;

                    if (_rootParent.GetType() == typeof(T))
                    {
                        _isSelfReference = true;
                    }
                    _rootParent = _rootParent.Parent;
                }
            }
        }

        /// <summary>
        /// Level depth from top-level parent.
        /// </summary>
        [XmlIgnore]
        public int NestingLevel
        {
            get { FindHierarchyPosition(); return _nestingLevel; }
            set { _nestingLevel = value; }
        }



        [XmlIgnore]
        public bool IsSelfReference
        {
            get { FindHierarchyPosition(); return _isSelfReference; }
            set { _isSelfReference = value; }
        }


        /// <summary>
        /// Get root (top-level) parent.
        /// </summary>
        [XmlIgnore]
        public PersistableDataObject RootParent
        {
            get
            {
                FindHierarchyPosition(); return _rootParent;
            }
        }


        /// <summary>
        /// Load or re-load a list from database.
        /// </summary>
        public virtual void Load()
        {
            DataSet ds;

            if (_isIQueryable)
            {
                // Update the sql load command to include paging information.
                IQueryable q = SkipTake(_query, (CurrentPage - 1) * PageSize, PageSize);
                _loadSql = q.ToString();
            }

            if (_loadCommand == CommandType.StoredProcedure)
            {
                ds = Session.GetDataset(this, _loadSql, _loadParameterValues);
            }
            else if (_loadCommand == CommandType.Text)
            {
                if (null != _loadParameterValues && _loadParameterValues.Length > 0)
                {
                    ds = Session.GetDataset(this, _loadCommand, _loadSql, _loadParameterValues);
                }
                else
                {
                    ds = Session.GetDataset(this, _loadCommand, _loadSql);
                }
            }
            else
            {
                throw new NotImplementedException("Unsupported CommandType in Load");
            }

            DataView dv = ds.Tables[0].DefaultView;

            _busy = true; // disable CollectionChanged events while loading

            List<T> newItems = new List<T>();
            using (TimedLock.Lock(SyncRoot))
            {
                bool currentReadOnly = IsImmutable;

                try
                {
                    IsImmutable = false;  // set false to allow Add's

                    _totalItemCount = dv.Table.Rows.Count;

                    if (_totalItemCount >= PageSize)
                    {
                        if (CurrentPage == 1)
                        {
                            IsMultiPaged = true;
                        }
                    }
                    else
                    {
                        if (IsMultiPaged)
                        {
                            IsLastPage = true;
                        }
                    }

                    _isOverflow = (_totalItemCount > PageSize);

                    // Use start and end index to guard against sprocs or sql commands returning unmanagably large datasets.
                    // IQueryable based lists should never return sets larger than PageSize.
                    int startIndex = 0;
                    int endIndex = _totalItemCount > PageSize ? PageSize : _totalItemCount;


                    List<T> toBeRemovedList = this.Items.ToList<T>();

                    for (int i = startIndex; i < endIndex; i++)
                    {
                        DataRow dr = dv.Table.Rows[i];
                        T item = PersistableDataObject.Create<T>(Session, Parent, dr);
                        item.PostLoadInitialize();
                        item.IsImmutable = currentReadOnly;

                        PersistableDataObject existingPDO = GetItemByPKeyID(item.PKeyID) as PersistableDataObject;

                        if (null == existingPDO)
                        {
                            this.Add(item);
                            newItems.Add(item);
                        }
                        else
                        {
                            existingPDO.CopyPersistedValues(item); // Update any new values to existing item

                            toBeRemovedList.Remove(existingPDO as T); // Clear item from to-be-removed list.
                        }
                    }


                    // Remove items from list that are no longer in database.
                    foreach (T r in toBeRemovedList)
                    {
                        Items.Remove(r);
                    }

                }
                catch (Exception ex)
                {
                    ErrorDispatcher.Instance.NotifyError(new ErrorNotifyEventArgs() { Message = ex.Message, StackTrace = ex.StackTrace });
                    throw; // ensure finally block does not swallow exceptions
                }
                finally
                {
                    IsImmutable = currentReadOnly;
                }
            }
            _busy = false; // re-enable CollectionChanged events 

            if (null != Parent && _isNew == true)
            {
                Parent.AddToSubscriptionChain(this);
            }
            _isNew = false;


            // Fire a collection changed Add event for single new item.

            // NOTE: We don't use the Add notification with a range of items, since CollectionViews
            // don't support that notification, and actually throw an exception if they receive one.
            if (newItems.Count == 1)
            {
                OnCollectionChanged(new System.Collections.Specialized.NotifyCollectionChangedEventArgs(
                    System.Collections.Specialized.NotifyCollectionChangedAction.Add, newItems));
            }
            else
            {
                // Fire a collection changed Reset event
                OnCollectionChanged(new System.Collections.Specialized.NotifyCollectionChangedEventArgs(
                        System.Collections.Specialized.NotifyCollectionChangedAction.Reset));
            }


        }


        /// <summary>
        /// Load an audit history list.
        /// This is different from a regular list load in that it does not attempt to add/remove items 
        /// from an existing list by pkey values, but rather adds all returned tuples as new items.
        /// </summary>
        public virtual void LoadAuditHistory()
        {
            DataSet ds;

            if (_loadCommand == CommandType.StoredProcedure)
            {
                ds = Session.GetDataset(this, _loadSql, _loadParameterValues);
            }
            else if (_loadCommand == CommandType.Text)
            {
                if (null != _loadParameterValues && _loadParameterValues.Length > 0)
                {
                    ds = Session.GetDataset(this, _loadCommand, _loadSql, _loadParameterValues);
                }
                else
                {
                    ds = Session.GetDataset(this, _loadCommand, _loadSql);
                }
            }
            else
            {
                throw new NotImplementedException("Unsupported CommandType in Load");
            }

            DataView dv = ds.Tables[0].DefaultView;

            _busy = true; // disable CollectionChanged events while loading

            using (TimedLock.Lock(SyncRoot))
            {
                try
                {
                    IsImmutable = false;  // set false to allow Add's

                    _totalItemCount = dv.Table.Rows.Count;

                    int startIndex = (CurrentPage - 1) * PageSize;
                    int endIndex = dv.Table.Rows.Count > PageSize ? PageSize : dv.Table.Rows.Count;
                    for (int i = startIndex; i < endIndex; i++)
                    {
                        DataRow dr = dv.Table.Rows[i];
                        T item = PersistableDataObject.Create<T>(Session, Parent, dr);

                        // Set AuditAction property on PersistableDataObject base.
                        item.AuditAction = (char)((string)dr["AuditAction"])[0];

                        item.PostLoadInitialize();
                        item.IsImmutable = true; // Audit records are always immutable
                        this.Add(item);
                    }
                }
                catch (Exception ex)
                {
                    ErrorDispatcher.Instance.NotifyError(new ErrorNotifyEventArgs() { Message = ex.Message, StackTrace = ex.StackTrace });
                    throw; // ensure finally block does not swallow exceptions
                }
                finally
                {
                    IsImmutable = true; // Audit list is always immutable
                }
            }
            _busy = false; // re-enable CollectionChanged events 

            _isNew = false;

            // Fire a collection changed Reset event
            OnCollectionChanged(new System.Collections.Specialized.NotifyCollectionChangedEventArgs(
                    System.Collections.Specialized.NotifyCollectionChangedAction.Reset));
        }


        /// <summary>
        /// Save (insert, update or delete) all items in list. 
        /// </summary>
        public bool Save()
        {
            using (TimedLock.Lock(SyncRoot))
            {
                bool success = true;
                foreach (PersistableDataObject pdo in this.Items)
                {
                    success &= pdo.SaveAll();
                }

                success &= FlushDeletedItems();

                _isDirty = false;
                _isNew = false;

                return success;
            }
        }

        /// <summary>
        /// Save (insert, update or delete) all items in list. 
        /// </summary>
        public bool Import()
        {
            using (TimedLock.Lock(SyncRoot))
            {
                bool success = true;
                foreach (PersistableDataObject pdo in this.Items)
                {
                    success &= pdo.ImportAll();
                }

                _isDirty = false;
                _isNew = false;

                return success;
            }
        }

        /// <summary>
        /// If the list has items contained within it.
        /// </summary>
        /// <remarks>
        /// MRS - I find this useful for binding, as opposed to writing a converter. The value is updated by OnCollectionChanged.
        /// </remarks>
        public bool HasItems
        {
            get
            {
                return (Count > 0);
            }
        }
        #endregion

        #region IPersistableDataObjectList implementation

        object IPersistableDataObjectList.this[int index]
        {
            get { return this[index] as object; }
            set { this[index] = value as T; }
        }

        object IPersistableDataObjectList.this[string index]
        {
            get { return this[index] as object; }
            set { this[index] = value as T; }
        }

        public void Add(object value)
        {
            Add(value as T);
        }

        [XmlIgnore]
        public PersistableDataObject Parent
        {
            get
            {
                return _parent;
            }
            set
            {
                _nestingLevel = -1;  // re-initialize to -1 to force RootParent re-determiniation
                _parent = value;
            }
        }

        [XmlIgnore]
        public PersistableDataObject InheritedParent
        {
            get
            {
                PersistableDataObject inheritedParent = Parent;
                while (null != inheritedParent && null != inheritedParent.Parent &&
                    inheritedParent.GetType() != typeof(PersistableDataObject))
                {
                    inheritedParent = inheritedParent.Parent;
                }
                return inheritedParent;
            }
        }

        [XmlIgnore]
        public string ParentForeignKeyName
        {
            get { return _parentForeignKeyName; }
        }

        [XmlIgnore]
        public string SourcePrimaryKeyName
        {
            get { return _sourcePrimaryKeyName; }
        }


        /// <summary>
        /// Delete from the database all items in list that are marked for deletion, and clear internal deleted items list.
        /// </summary>
        public bool FlushDeletedItems()
        {
            bool success = true;
            if (null != _deletedItems)
            {
                using (TimedLock.Lock(SyncRoot))
                {
                    foreach (PersistableDataObject pdo in _deletedItems)
                    {
                        success |= pdo.Delete();
                    }

                    if (success)
                    {
                        _deletedItems = null;
                    }
                }
            }
            return success;
        }


        /// <summary>
        /// Called by PersistableDataObjectXmlSerializer prior to serialization.
        /// Provides opportunity to add items to IncludePropertyPath, ExcludeType, etc.
        /// </summary>
        virtual public void PrepareSerialization()
        {
            // no-op in base class
        }

        #endregion

        #region Base class overrides

        private bool? _autoExpand;

        /// <summary>
        /// Controls automatic expansion of contained list elements.
        /// When true, automatically expands lists to ensure expressions such as "foo.bar[3]" 
        /// have enough elements to prevent "index out of range" exceptions.
        /// </summary>
        [XmlIgnore]
        public bool AutoExpand
        {
            get
            {
                if (!_autoExpand.HasValue)
                {
                    if (Parent == null)
                    {
                        _autoExpand = false;
                    }
                    else
                    {
                        _autoExpand = Parent.AutoExpand;
                    }
                }

                return _autoExpand.Value;
            }

            set { _autoExpand = value; }
        }


        /// <summary>
        /// Indexer that supports automatic expansion to size required to support specified index value.
        /// When AutoExpand is true, this list will expand as needed to accommodate index.
        /// </summary>
        /// <param name="index">index</param>
        /// <returns></returns>
        new public virtual T this[int index]
        {
            get
            {
                if (index >= this.Count && AutoExpand)
                {
                    while (this.Count <= index)
                    {
                        T pdo = new T();
                        CreateChildRelationships(pdo as PersistableDataObject);
                        base.Add(pdo);
                    }
                }

                return base[index];
            }

            set
            {
                if (index >= this.Count && AutoExpand)
                {
                    while (this.Count <= index)
                    {
                        base.Add(new T());
                    }
                }

                base[index] = value;
            }
        }



        public PersistableDataObject CreateNewItem()
        {
            string typeName = typeof(T).FullName;
            return PersistableDataObject.Create(Parent, typeName);
        }



        private static IQueryable SkipTake(IQueryable query, int skip, int take)
        {
            Type t = query.ElementType;
            Type ex = typeof(Queryable);

            MethodInfo miSkip = ex.GetMethod("Skip");
            MethodInfo miTake = ex.GetMethod("Take");
            MethodInfo miSkipConstructed = miSkip.MakeGenericMethod(t);
            MethodInfo miTakeConstructed = miTake.MakeGenericMethod(t);

            object[] args = { query, skip };
            IQueryable q = (IQueryable)miSkipConstructed.Invoke(null, args);

            args = new object[] { q, take };
            q = (IQueryable)miTakeConstructed.Invoke(null, args);


            return q;
        }


        /// <summary>
        /// Return list of items meeting provided selection criteria.
        /// </summary>
        /// <param name="selectCriteria"></param>
        /// <returns></returns>
        private List<T> GetPropertyMatchList(string selectCriteria)
        {
            List<T> list;
            const char splitChar = '&';
            char[] splitCharArray = { splitChar };
            if (selectCriteria.Contains(splitChar)) // Multiple, splitChar delimted property name/value pairs.
            {
                string[] criterion = selectCriteria.Split(splitCharArray, StringSplitOptions.RemoveEmptyEntries);
                list = this.ToList<T>();
                foreach (string instruction in criterion)
                {
                    list = GetPropertyMatchList(list, instruction);
                }
            }
            else // Single name/value pair.
            {
                list = GetPropertyMatchList(this.ToList<T>(), selectCriteria);
            }
            return list;
        }

        /// <summary>
        /// Return list of items from source list matching selection criteria.
        /// </summary>
        /// <param name="sourceList">source list </param>
        /// <param name="selectCriteria">selection criteria</param>
        /// <returns>list of matching items</returns>
        private List<T> GetPropertyMatchList(List<T> sourceList, string selectCriteria)
        {
            string key;
            string val;
            try
            {
                string[] delims = { "=" };
                string[] spec = selectCriteria.Split(delims, StringSplitOptions.RemoveEmptyEntries);

                key = spec[0].Trim();
                val = spec[1].Trim();
            }
            catch (Exception ex)
            {
                throw new InvalidOperationException("Error parsing PersistableDataObjectList key-value index argument.", ex);
            }



            //// Example using a Dynamic Linq query:
            //      string qformat = string.Format("{0}.ToString() == @0", key);
            //      List<T> resultList = this.AsQueryable().Where(qformat, val).ToList<T>(); 
            //      return resultList;
            //
            // NOTE: This works, but is much slower than the simple reflection based value 
            // comparison technique used below.


            // Simple reflection based value comparison.
            // NOTE: Could be optimized by saving and re-using property getter MethodInfo.
            List<T> list = new List<T>();
            foreach (T pdo in sourceList)
            {
                string testValue = ReflectionHelper.GetObjectPropertyValue(pdo, key).ToString();
                if (null != testValue && testValue.Equals(val))
                {
                    list.Add(pdo);
                }
            }
            return list;
        }



        /// <summary>
        /// Find a particular item with a named property of specified value, 
        /// or property of a specific type at a numerical location.
        /// </summary>
        /// <param name="indexInstruction">index instruction of form propertyName=value, or index::TypeName</param>
        /// <returns>index of matching property</returns>
        private int GetIndexFromInstruction(string indexInstruction)
        {
            int index = -1;
            string typeName = typeof(T).FullName; // default to base Type

            if (indexInstruction.Contains("::"))
            {
                // Add new item at specified index of particular derived type.
                // Index instruction of form:  <index>::<TypeName>

                try
                {
                    string[] delims = { "::" };
                    string[] spec = indexInstruction.Split(delims, StringSplitOptions.None);

                    index = Convert.ToInt32(spec[0]);
                    typeName = spec[1];
                }
                catch (Exception ex)
                {
                    throw new InvalidOperationException("Error parsing list item extended-type index argument.", ex);
                }

                if (index >= this.Count && AutoExpand)
                {
                    while (this.Count <= index)
                    {
                        base.Add(PersistableDataObject.Create(Parent, typeName) as T);
                    }
                }
            }
            else if (indexInstruction.Contains("="))
            {
                // Find index where named property matches value.
                // Index instruction of form:  <property>=<property value>

                List<T> list = GetPropertyMatchList(indexInstruction);

                if (list.Count > 1)
                {
                    throw new InvalidOperationException("Multiple items matching index critera.");
                }

                if (list.Count == 1)
                {
                    index = this.IndexOf(list[0]);
                }
            }
            else
            {
                // Just parse string to integer value.

                try
                {
                    index = int.Parse(indexInstruction);
                }
                catch (Exception ex)
                {
                    throw new InvalidOperationException("Error parsing string list index argument.", ex);
                }
            }


            if (index >= this.Count && AutoExpand)
            {
                while (this.Count <= index)
                {
                    base.Add(PersistableDataObject.Create(Parent, typeName) as T);
                }
            }

            return index;
        }

        /// <summary>
        /// Return first item matching selction critera, or default.
        /// </summary>
        /// <param name="selectionCriteria">selction critera</param>
        /// <returns>first item, or null</returns>
        public T GetFirst(string selectionCriteria)
        {
            List<T> list = GetPropertyMatchList(selectionCriteria);

            if (list.Count > 0)
            {
                return list[0];
            }

            return default(T);
        }


        /// <summary>
        /// Indexer that supports a wide range of index instructions, allowing filtering and new item creation.
        ///    Add new item at specified index of particular derived type. E.g. {index}::{TypeName}
        ///    Find item where named property matches value.  E.g. {property}={property value}
        /// </summary>
        /// <param name="index">indexInstruction</param>
        /// <returns>specified list item</returns>
        public virtual T this[string indexInstruction]
        {
            get
            {
                int index = GetIndexFromInstruction(indexInstruction);

                if (index > -1)
                {
                    return base[index];
                }
                return null;
            }

            set
            {
                int index = GetIndexFromInstruction(indexInstruction);
                base[index] = value;
            }
        }


        /// <summary>
        /// Set parent / child relationships for newly added list item.
        /// </summary>
        /// <param name="pdo">new list item</param>
        private void CreateChildRelationships(PersistableDataObject pdo)
        {
            pdo.ListContainer = this;
            pdo.Parent = this.Parent;
            pdo.Session = Session; // ensure child shares session of parent

            // If we don't have a ParentForeignKeyName defined, this list was not created 
            // from generated code as a child list; i.e. it has been created new in application code.
            if (ParentForeignKeyName != null)
            {
                if (null != Parent && typeof(PersistableDataObject).IsAssignableFrom(Parent.GetType()))
                {
                    Parent.MarkListContentsDirty();

                    // Wire-up a delegate that we'll call later after saving a new parent which
                    // sets parent foreign key info.
                    pdo.ForeignKeyUpdateHandler += delegate(object key)
                    { ReflectionHelper.CopyObjectPropertyValue(Parent, SourcePrimaryKeyName, pdo, ParentForeignKeyName); };

                }
            }

            _isDirty = true;
        }


        /// <summary>
        /// Clone (deep copy) all items from provided list and add to this list.
        /// </summary>
        /// <param name="list"></param>
        public void Add(PersistableDataObjectList<T> list)
        {
            foreach (PersistableDataObject pdo in list)
            {
                T newPdo = new T();
                newPdo.CopyPersistedValues(pdo);
                this.Add(newPdo);
            }
        }


        /// <summary>
        /// Add a new PersistableDataObject to this list, setting up foreign key managment.
        /// </summary>
        /// <param name="value">PersistableDataObject object to add.</param>
        new public virtual void Add(T value)
        {
            if (IsImmutable)
            {
                throw new InvalidOperationException("List is immutable readonly.");
            }

            PersistableDataObject item = value as PersistableDataObject;
            CreateChildRelationships(item);

            using (TimedLock.Lock(SyncRoot))
            {
                base.Add(value);
            }


            if (!item.IsNew)
            {
                // Chain in any notification subscriptions, but only when loading from 
                // database (i.e. when !IsNew).
                // This Add method is also used to add new items to a list prior to saving, but in 
                // that case we don't want to subscribe to notifications until after the items'
                // primary key has been set, which happens as a result of saving.

                item.RootParent.AddToSubscriptionChain(item);

                // For non-list item PDOs, AddToSubscriptionChain is called from base.Load().
            }

        }

        /// <summary>
        /// Removes all elements from this list.  Any items previously marked for deletion are also 
        /// removed *without* performing delete operation.
        /// </summary>
        new public virtual void Clear()
        {
            if (IsImmutable)
            {
                throw new InvalidOperationException("List is readonly.");
            }

            if (null == _deletedItems)
            {
                _deletedItems = null;
            }

            base.Clear();

            _isDirty = false;

        }


        /// <summary>
        /// Remove specified item from list, and add it to internally maintained list of deleted items.  
        /// The internal deleted items list is used to actually perform database deletes on next Save.
        /// </summary>
        /// <param name="value"></param>
        new public virtual void Remove(T value)
        {
            if (IsImmutable)
            {
                throw new InvalidOperationException("List is readonly.");
            }

            if (null == _deletedItems)
            {
                _deletedItems = new PersistableDataObjectList<T>(Session, Parent, _parentForeignKeyName, _sourcePrimaryKeyName,
                    _loadCommand, _loadSql, _loadParameterValues);
            }

            using (TimedLock.Lock(SyncRoot))
            {
                PersistableDataObject item = value as PersistableDataObject;

                item.MarkedForDeletion = false;

                if (Parent != null)
                {
                    // Mark parent dirty making sure not to trip an exception 
                    // for modifying a PDO already marked for deletion.

                    bool parentDirtyState = Parent.MarkedForDeletion;
                    Parent.MarkedForDeletion = false;

                    Parent.MarkListContentsDirty();

                    Parent.MarkedForDeletion = parentDirtyState;
                }

                _deletedItems.Add(value);

                base.Remove(value);

                item.MarkedForDeletion = true;

                _isDirty = true;
            }
        }


        /// <summary>
        /// Extract an item from a list without deleting it from data source.  
        /// </summary>
        /// <param name="value">value to extract from list</param>
        public void Extract(T value)
        {
            if (IsImmutable)
            {
                throw new InvalidOperationException("List is readonly.");
            }

            using (TimedLock.Lock(SyncRoot))
            {
                PersistableDataObject item = value as PersistableDataObject;

                if (Parent != null)
                {
                    // Mark parent dirty making sure not to trip an exception 
                    // for modifying a PDO already marked for deletion.

                    bool parentDirtyState = Parent.MarkedForDeletion;
                    Parent.MarkedForDeletion = false;

                    Parent.MarkListContentsDirty();

                    Parent.MarkedForDeletion = parentDirtyState;
                }

                base.Remove(value);

                // null out list foreign key property
                string n = ParentForeignKeyName;
                if (null != n) // only if there is a parent foreign key
                {
                    ReflectionHelper.SetObjectPropertyValue(value, n, null);
                }

                _isDirty = true;
            }
        }


        /// <summary>
        /// Remove specified item from list, and add it to internally maintained list of deleted items.  
        /// The internal deleted items list is used to actually perform database deletes on next Save.
        /// </summary>
        /// <param name="value"></param>
        new public virtual void RemoveAt(int index)
        {
            if (IsImmutable)
            {
                throw new InvalidOperationException("List is readonly.");
            }

            if (null == _deletedItems)
            {
                _deletedItems = new PersistableDataObjectList<T>(Session, Parent, _parentForeignKeyName, _sourcePrimaryKeyName,
                    _loadCommand, _loadSql, _loadParameterValues);
            }

            T item = Items[index];
            item.MarkedForDeletion = true;

            _deletedItems.Add(item);

            base.Remove(item);

            _isDirty = true;
        }

        /// <summary>
        /// Remove all items from list, delete each item from the database, and clear any cached child list items.
        /// </summary>
        public void Delete()
        {
            using (TimedLock.Lock(SyncRoot))
            {
                // separate copy of internal list 
                List<T> deleteItemList = this.ToList<T>();

                foreach (T pdo in deleteItemList)
                {
                    this.Remove(pdo);
                }
                this.MarkedForDeletion = true;
            }
            this.Save();
        }


   
        public override event NotifyCollectionChangedEventHandler CollectionChanged;
        protected override void OnCollectionChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            //if (!_busy || !_sendNotificationsWhileLoading)
            if (!_busy)
            {
                // Be nice - use BlockReentrancy like MSDN said
                using (BlockReentrancy())
                {
                    OnPropertyChanged(new PropertyChangedEventArgs("HasItems"));

                    System.Collections.Specialized.NotifyCollectionChangedEventHandler eventHandler = CollectionChanged;
                    if (eventHandler == null)
                        return;

                    Delegate[] delegates = eventHandler.GetInvocationList();
                    // Walk thru invocation list
                    foreach (System.Collections.Specialized.NotifyCollectionChangedEventHandler handler in delegates)
                    {
                        DispatcherObject dispatcherObject = handler.Target as DispatcherObject;
                        // If the subscriber is a DispatcherObject and different thread
                        if (dispatcherObject != null && dispatcherObject.CheckAccess() == false)
                        {
                            // Invoke handler in the target dispatcher's thread
                            dispatcherObject.Dispatcher.Invoke(DispatcherPriority.DataBind, handler, this, e);
                        }
                        else // Execute handler as is
                            handler(this, e);
                    }
                }
            }
        }


        /// <summary>
        /// Copy the item matching the predicate function from the source list, if a matching item is found.  
        /// If no matching item is found, nothing is copied.
        /// If more than one matching item is found, an exception is thrown.
        /// </summary>
        /// <param name="sourceList">source list</param>
        /// <param name="predicate">predicate function</param>
        public void CopyItem(PersistableDataObjectList<T> sourceList, Func<T, bool> predicate)
        {

            T pdo = sourceList.Where(predicate).SingleOrDefault();
            if (null != pdo)
            {
                T newItem = pdo.Clone() as T;
                newItem.SeverPersistenceRelationships();
                this.Add(newItem);
            }
        }

        /// <summary>
        /// Copy the item matching the selection critera from the source list, if a matching item is found.  
        /// If no matching item is found, nothing is copied.
        /// If more than one matching item is found, an exception is thrown.
        /// </summary>
        /// <param name="sourceList">source list</param>
        /// <param name="selectionCriteria">selection criteria</param>
        public void CopyItem(IPersistableDataObjectList sourceList, string selectionCriteria)
        {
            PersistableDataObject pdo = sourceList[selectionCriteria] as PersistableDataObject;
            if (null != pdo)
            {
                PersistableDataObject newItem = pdo.Clone() as PersistableDataObject;
                newItem.SeverPersistenceRelationships();
                this.Add(newItem);
            }
        }


        #endregion

        #region Sorting

        // NOTE: The ObservableCollection base class does not directly support sorting, hence this
        // simplistic implementation.  Likely candidate for optimization.

        public void Sort()
        {
            if (null != _defaultComparer)
            {
                Sort(_defaultComparer);
            }
        }

        public void Sort(IComparer comparer)
        {
            _defaultComparer = comparer;

            int i, j;
            T index;
            for (i = 1; i < Count; i++)
            {
                index = this[i];
                j = i;

                while ((j > 0) && (comparer.Compare(this[j - 1], index) == 1))
                {
                    this[j] = this[j - 1];
                    j = j - 1;
                }

                this[j] = index;
            }
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                // free managed resources
                if (Parent != null)
                {
                    Parent = null;
                }
            }
        }

        #endregion

        #region ICloneable Members


        public object Clone()
        {
            Dictionary<int, IPersistableData> callTrace = new Dictionary<int, IPersistableData>();
            return Clone(callTrace);
        }

        public IPersistableData Clone(Dictionary<int, IPersistableData> callTrace)
        {
            PersistableDataObjectList<T> newList = new PersistableDataObjectList<T>(Session, Parent, _parentForeignKeyName, _sourcePrimaryKeyName,
                                _loadCommand, _loadSql, _loadParameterValues);

            newList.ChangeTrackingEnabled = false;

            foreach (PersistableDataObject pdo in Items)
            {
                newList.Add(pdo.Clone(callTrace) as T);
            }

            newList.ChangeTrackingEnabled = true;

            return newList;
        }


        public IPersistableData Twin(PersistableDataObject parent)
        {
            PersistableDataObjectList<T> newList = new PersistableDataObjectList<T>(Session, parent, _parentForeignKeyName, _sourcePrimaryKeyName,
                    _loadCommand, _loadSql, _loadParameterValues);

            newList.ChangeTrackingEnabled = false;

            foreach (PersistableDataObject pdo in Items)
            {
                T newPdo = pdo.Twin(parent) as T;
                newList.Add(newPdo);
                newPdo.SeverPersistenceRelationships();
            }

            newList.ChangeTrackingEnabled = true;

            return newList;
        }



        #endregion

    }
}
